/*
 * Iparelan ExcepGen
 * This file is part of ExcepGen.
 * Copyright (c) 2008, 2009, Iparelan Solutions, LLC. All rights reserved.
 *
 * ExcepGen is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * ExcepGen is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ExcepGen. If not, see <http://www.gnu.org/licenses/>.
 */

package com.iparelan.excepgen;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Writer;

import java.net.URL;
import java.net.URLClassLoader;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;

import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;

import java.util.logging.Logger;

import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINEST;
import static java.util.logging.Level.WARNING;

import javax.tools.ToolProvider;
import javax.tools.JavaCompiler;

import edu.umd.cs.findbugs.annotations.Nullable;

import net.jcip.annotations.NotThreadSafe;

import static com.iparelan.excepgen.ExcepGen.LINE_SEPARATOR;

import static com.iparelan.excepgen.L7dMsg.MSGS;

/**
 * An {@code ExceptionSpecification} contains attributes of an exception
 * type and uses those attributes to {@link #write(java.io.File) generate
 * Java source code}.
 *
 * @author Greg Mattes
 * @version September 2008
 */
@NotThreadSafe
final class ExceptionSpecification {
    /** The filename extension of all Java source files. */
    public static final String JAVA_EXTENSION = ".java";

    /** The ExcepGen {@link java.util.logging.Logger}. */
    private static final Logger LOGGER = Logger.getLogger(ExcepGen.class.getSimpleName());

    /**
     * The number of spaces to use when indenting generated code ({@value}).
     */
    private static final int INDENT_SPACE_COUNT = 4;

    /**
     * String consisting of {@value #INDENT_SPACE_COUNT} spaces used to indent
     * generated code.
     */
    private static final String INDENT;
    static {
        final char[] indent = new char[INDENT_SPACE_COUNT];
        Arrays.fill(indent, ' ');
        INDENT = new String(indent);
    }

    /**
     * String consisting of double the number of spaces as {@link #INDENT}
     * used to double indent generated code.
     */
    private static final String DOUBLE_INDENT = (INDENT + INDENT);

    /** The package in which the generated exception resides. */
    private String packageName;

    /** The name of the generated exception type. */
    private String className;

    /** The name of the superclass of the generated exception. */
    private String superClassName;

    /**
     * The non-Javadoc documentation to place at the top of the generated
     * file.
     */
    private String documentationHeader;

    /**
     * The plain-text license content to place at the top of the generated
     * file after the documentation header.
     */
    private String license;

    /** The Javadoc class documentation for the generated exception. */
    private String documentation;

    /**
     * The "extra code" that is appended to a generated exception source code
     * file.
     */
    private String extraCode;

    /** The names of the subtypes of this exception. */
    private final Collection<String> subTypeNames = new HashSet<String>();

    /**
     * Creates a new {@code ExceptionSpecification}.
     *
     * @param className
     *
     *        The name of the generated class. May not be {@code null}.
     *
     * @param packageName
     *
     *        The name of the package of the generated class. May not be
     *        {@code null}.
     *
     * @param superClassName
     *
     *        The name of the superclass of the generated class. May not be
     *        {@code null}.
     *
     * @param documentationHeader
     *
     *        The documentation to place at the top of the generated file. May
     *        not be {@code null}.
     *
     * @param license
     *
     *        The license to place at the top of the generated file after the
     *        documentation header. It is assumed that {@code license} is
     *        either the empty string or has line separators after every line
     *        including the final line and has a single space and an
     *        asterisk/splat preceding each line for inclusion as part of a
     *        Javadoc comment. May not be {@code null}.
     *
     * @param documentation
     *
     *        The documentation for the generated class. May not be {@code
     *        null}.
     */
    ExceptionSpecification(final String className, final String packageName, final String superClassName, final String documentationHeader, final String license, final String documentation) {
        this.className = className;
        this.packageName = packageName;
        this.superClassName = superClassName;
        this.documentationHeader = documentationHeader;
        this.license = license;
        this.documentation = documentation;
    }

    /**
     * A "hook" that allows the specification of additional code that will be
     * generated in the exception java source code file.
     *
     * <p>Any extra code must not depend on any code in any other
     * files. That is, the exception source file must be able to be
     * compiled by itself so that ExcepGen can compute its serialVersionUID.
     *
     * @param extraCode
     *
     *        The extra java source code. May not be {@code null}.
     */
    public void setExtraCode(final String extraCode) {
        this.extraCode = extraCode;
    }

    /**
     * Adds a subtype name to the collection of subtype names.
     *
     * @param subTypeName
     *
     *        The subtype name. May not be {@code null}.
     */
    public void addSubTypeName(final String subTypeName) {
        this.subTypeNames.add(subTypeName);
    }

    @Override
    // BEGIN SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore) justification: custom formatting
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("[").append(getClass().getSimpleName()).append(": ");
        sb
            .append("type(").append(packageName).append(".").append(className).append(" extends ").append(superClassName)
            ;
        for (String subTypeName : subTypeNames) {
            sb.append("), subTypeName(").append(subTypeName);
        }
        sb
            .append("), doc-header(").append(documentationHeader)
            .append("), license(").append(license)
            .append("), doc(").append(documentation)
            .append(")")
            ;
        sb.append("]");
        return sb.toString();
    }
    // END SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore)

    /**
     * Writes Java code for the {@code Exception} specified by this {@code
     * ExceptionSpecification} in a file in the appropriate subdirectory
     * of {@code targetDirectory} according to the {@code packageName} of
     * this {@code ExceptionSpecification}.
     *
     * <p>Any I/O errors are logged, but not propagated to the caller. No
     * generated file is produced if an I/O error occurs.
     *
     * @param targetDirectory
     *
     *        The directory in which to generate Java code for the {@code
     *        Exception} specified by this {@code
     *        ExceptionSpecification}. May not be {@code null}.
     */
    @edu.umd.cs.findbugs.annotations.SuppressWarnings(
        value = "NP_NULL_PARAM_DEREF_NONVIRTUAL",
        justification = "(false positive)"
    )
    public void write(final File targetDirectory) {
        File targetFile = null;
        try {
            targetFile = getTargetFile(targetDirectory);
            final Long bogusTemporarySerialVersionUID = null;
            write(targetFile, bogusTemporarySerialVersionUID);
            final long serialVersionUID = getSerialVersionUID(targetDirectory, targetFile);
            write(targetFile, serialVersionUID);
        } catch (final IOException ioe) {
            final String errMsg = MSGS.get(L7dMsg.IO_ERROR);
            LOGGER.log(WARNING, errMsg, ioe);
            if (targetFile != null) {
                try {
                    final boolean success = targetFile.delete();
                    if (! success) {
                        final String deleteErrMsg = MSGS.get(L7dMsg.FILE_DELETE_DENIED, targetFile);
                        LOGGER.log(WARNING, deleteErrMsg);
                    }
                } catch (final SecurityException se) {
                    final String deleteErrMsg = MSGS.get(L7dMsg.FILE_DELETE_DENIED, targetFile);
                    LOGGER.log(WARNING, deleteErrMsg, se);
                }
            }
        }
    }

    /**
     * Writes Java code for the {@code Exception} specified by this {@code
     * ExceptionSpecification} in a file in either the directory {@code
     * targetDirectory} or some sub-directory of {@code targetDirectory}
     * as determined by the {@code packageName} of this {@code
     * ExceptionSpecification}.
     *
     * @param targetFile
     *
     *        The file in which to generate Java code for the {@code
     *        Exception} specified by this {@code
     *        ExceptionSpecification}. May not be {@code null}.
     *
     * @param serialVersionUID
     *
     *        The serial version UID to write to the generated file. May
     *        be {@code null} which means "do not write a
     *        serialVersionUID."
     *
     * @throws IOException
     *
     *         Iff some I/O error occurs.
     */
    private void write(final File targetFile, @Nullable final Long serialVersionUID) throws IOException {
        Writer writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(targetFile));
            final String javaCode = generateJavaCode(serialVersionUID);
            LOGGER.log(FINEST, javaCode);
            writer.write(javaCode);
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (final IOException ioe) {
                final String errMsg = MSGS.get(L7dMsg.IO_ERROR_ON_CLOSE);
                LOGGER.log(WARNING, errMsg, ioe);
            }
        }
    }

    /**
     * Tells whether the exception described by this {@code
     * ExceptionSpecification} has subtypes.
     *
     * @return {@code true} if the exception described by this {@code
     *         ExceptionSpecification} has subtypes, {@code false} otherwise.
     */
    private boolean hasSubTypes() {
        return (! subTypeNames.isEmpty());
    }

    /**
     * Produces the {@code serialVersionUID} for the generated file.
     *
     * @param targetDirectory
     *
     *        The directory in which to generate Java code for the {@code
     *        Exception} specified by this {@code
     *        ExceptionSpecification}. May not be {@code null}.
     *
     * @param targetFile
     *
     *        The file in which to generate Java code for the {@code
     *        Exception} specified by this {@code
     *        ExceptionSpecification}. May not be {@code null}.
     *
     * @return The {@code serialVersionUID} for the generated file.
     *
     * @throws IOException
     *
     *         Iff some I/O error occurs.
     *
     * @see <nobr><a href="http://java.sun.com/javase/6/docs/platform/serialization/spec/serialTOC.html">Java Object Serialization Specification</a>, <a href="http://java.sun.com/javase/6/docs/platform/serialization/spec/class.html">Section 4, "Class Descriptors"</a></nobr>
     *
     * @to.do
     *
     * <ul>
     *
     * <li>Split this method into a compiler step and a serialver step.
     *
     * <li>The generation of the serial version id should be a function of
     *     some policy:
     *
     *     <ol>
     *
     *     <li>Don't specify a serial version id (emit a compiler
     *         {@code SuppressWarnings("serial")}).
     *
     *     <li>Use serialver to compute a serial version id.
     *
     *     <li>Embed some value specified in the ExcepGen config file.
     *
     *     </ol>
     *
     * </ul>
     */
    // BEGIN SuppressWarnings(Checkstyle.ExecutableStatementCount) justification: none, this should be refactored
    private long getSerialVersionUID(final File targetDirectory, final File targetFile) throws IOException {
        String javaClassFilenameRelative = null;
        String javaSubTypeClassFilenameRelative = null;
        try {
            final JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
            if (javac == null) {
                final String logMsg = MSGS.get(L7dMsg.COMPILER_LOADER_ERROR);
                throw new IOException(logMsg);
            }
            final InputStream in = null; // use standard in
            final OutputStream out = null; // use standard out
            final OutputStream err = null; // use standard err
            final String javaSourceFilenameAbsolute = targetFile.getCanonicalPath();
            assert (javaSourceFilenameAbsolute.endsWith(JAVA_EXTENSION));
            final String targetDirectoryPathAbsolute = targetDirectory.getCanonicalPath();
            assert (javaSourceFilenameAbsolute.startsWith(targetDirectoryPathAbsolute));
            final String[] arguments = new String[] {
                javaSourceFilenameAbsolute,
                "-d",
                targetDirectoryPathAbsolute,
            };
            final int javacStatus = javac.run(in, out, err, arguments);
            final String logMsg = MSGS.get(L7dMsg.COMPILER_STATUS, javacStatus);
            final boolean javacSuccess = (javacStatus == 0);
            if (javacSuccess) {
                LOGGER.log(FINE, logMsg);
            } else {
                throw new IOException(logMsg);
            }

            final int    targetDirectoryPathLength               = targetDirectoryPathAbsolute.length();
            final int    javaSourceFilenameRelativePosition      = (targetDirectoryPathLength + File.pathSeparator.length());
            final String javaSourceFilenameRelative              = javaSourceFilenameAbsolute.substring(javaSourceFilenameRelativePosition);
            final int    javaExtensionPosition                   = javaSourceFilenameRelative.lastIndexOf(JAVA_EXTENSION);
            final String javaSourceFilenameRelativeSansExtension = javaSourceFilenameRelative.substring(0, javaExtensionPosition);
            final String javaClassname                           = javaSourceFilenameRelativeSansExtension.replace(File.separatorChar, '.');

            javaClassFilenameRelative = javaSourceFilenameRelativeSansExtension + ".class";
            if (hasSubTypes()) {
                javaSubTypeClassFilenameRelative = javaSourceFilenameRelativeSansExtension + "$SubType.class";
            }

            // Trailing slash implies directory rather than jar for URLClassLoader.
            final String targetDirectoryPathAbsoluteURL = "file:///" + targetDirectoryPathAbsolute + "/";

            final URL[] classpath = new URL[] {
                new URL(targetDirectoryPathAbsoluteURL),
            };
            final ClassLoader classLoader = new URLClassLoader(classpath);
            final boolean initializeClass = false;
            final Class<?> exceptionClass = Class.forName(javaClassname, initializeClass, classLoader);
            final ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(exceptionClass);
            final long serialVersionUID = objectStreamClass.getSerialVersionUID();

            return serialVersionUID;
        } catch (final ClassNotFoundException cnfe) {
            final String logMsg = MSGS.get(L7dMsg.CLASS_LOADING_FAILURE, javaClassFilenameRelative);
            throw new IOException(logMsg, cnfe);
        } finally {
            final String[] filenamesToDelete = new String[] {
                javaClassFilenameRelative,
                javaSubTypeClassFilenameRelative,
            };
            for (String filename : filenamesToDelete) {
                final boolean doDeletion = (filename != null);
                if (doDeletion) {
                    deleteFileByRelativeFilename(targetDirectory, filename);
                }
            }
        }
    }
    // END SuppressWarnings(Checkstyle.ExecutableStatementCount)

    /**
     * Attempts to delete the file specified by {@code relativeFilename}
     * in {@code directory}. If the file cannot be deleted a log message
     * is produced, but no error is thrown.
     *
     * @param directory
     *
     *        The directory of the file that is deleted. May not be {@code
     *        null}.
     *
     * @param relativeFilename
     *
     *        The filename relative to {@code directory} that is
     *        deleted. May not be {@code null}.
     *
     * @return {@code true} if the file was deleted, {@code false}
     *         otherwise.
     */
    private boolean deleteFileByRelativeFilename(final File directory, final String relativeFilename) {
        final boolean fileDeleteSuccess;
        final File toDelete = new File(directory, relativeFilename);
        try {
            final boolean fileExists = toDelete.exists();
            if (fileExists) {
                fileDeleteSuccess = toDelete.delete();
                final boolean fileDeleteFailure = (! fileDeleteSuccess);
                if (fileDeleteFailure) {
                    final String deleteErrMsg = MSGS.get(L7dMsg.FILE_DELETE_DENIED, toDelete);
                    LOGGER.log(WARNING, deleteErrMsg);
                }
            } else {
                fileDeleteSuccess = false;
            }
            return fileDeleteSuccess;
        } catch (final SecurityException se) {
            final String deleteErrMsg = MSGS.get(L7dMsg.FILE_DELETE_DENIED, toDelete);
            LOGGER.log(WARNING, deleteErrMsg, se);
            return false;
        }
    }

    /**
     * Produces Java code for the {@code Exception} specified by this
     * {@code ExceptionSpecification}.
     *
     * @param serialVersionUID
     *
     *        The serial version UID to write to the generated file. May
     *        be {@code null} which means "do not generate a
     *        serialVersionUID."
     *
     * @return Java code for the {@code Exception} specified by this
     *         {@code ExceptionSpecification}. Never {@code null}.
     *
     * @see <nobr><a href="http://java.sun.com/javase/6/docs/platform/serialization/spec/serialTOC.html">Java Object Serialization Specification</a>, <a href="http://java.sun.com/javase/6/docs/platform/serialization/spec/class.html">Section 4, "Class Descriptors"</a></nobr>
     */
    // BEGIN SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore) justification: custom formatting
    private String generateJavaCode(@Nullable final Long serialVersionUID) {
        final StringBuilder sb = new StringBuilder();
        /*
         * Generate the top matter of the file: header documentation,
         * package declaration, import statements, and type Javadoc.
         */
        sb
            .append("/*")                                       .append(LINE_SEPARATOR)
            .append(" * ").append(documentationHeader)          .append(LINE_SEPARATOR)
            .append(license)
            .append(" */")                                      .append(LINE_SEPARATOR) // SuppressWarnings(PMD.AvoidDuplicateLiterals) justification: refactoring would increase obscurity
            .append(LINE_SEPARATOR)
            .append("package ").append(packageName).append(";") .append(LINE_SEPARATOR)
            .append(LINE_SEPARATOR)
            .append("import javax.annotation.Generated;")       .append(LINE_SEPARATOR)
            .append(LINE_SEPARATOR)
            .append("/**")                                      .append(LINE_SEPARATOR) // SuppressWarnings(PMD.AvoidDuplicateLiterals) justification: refactoring would increase obscurity
            .append(" * ").append(documentation)                .append(LINE_SEPARATOR)
            .append(" */")                                      .append(LINE_SEPARATOR)
            ;

        // Generate the "Generated" annotation "value" element value with ISO 8601 timestamp
        final String dateFormatISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
        final DateFormat dateFormat = new SimpleDateFormat(dateFormatISO8601);
        final Date now = new Date();
        final FieldPosition notInteresting = new FieldPosition(DateFormat.DATE_FIELD);
        final StringBuffer stringBuffer = dateFormat.format(now, new StringBuffer(), notInteresting);
        final String timestamp = stringBuffer.toString();
        final String generatorName = ExcepGen.class.getName();
        sb
            .append("@Generated (")                                                  .append(LINE_SEPARATOR)
            .append(INDENT).append("value = \"").append(generatorName).append("\",") .append(LINE_SEPARATOR)
            .append(INDENT).append("date = \"").append(timestamp).append("\"")       .append(LINE_SEPARATOR)
            .append(")")                                                             .append(LINE_SEPARATOR)
            ;

        sb.append("public class ").append(className).append(" extends ").append(superClassName).append(" {").append(LINE_SEPARATOR);

        final boolean generateSerialVersionUID = (serialVersionUID != null);
        if (generateSerialVersionUID) {
            sb.append(INDENT).append("static final long serialVersionUID = ").append(serialVersionUID).append("L;").append(LINE_SEPARATOR);
            sb.append(LINE_SEPARATOR);
        }

        if (hasSubTypes()) {
            sb.append(INDENT).append("public enum SubType {")            .append(LINE_SEPARATOR);
            for (String subTypeName : subTypeNames) {
                sb.append(DOUBLE_INDENT).append(subTypeName).append(",") .append(LINE_SEPARATOR);
            }
            sb
                .append(INDENT).append("}")                              .append(LINE_SEPARATOR)
                .append(LINE_SEPARATOR)
                .append(INDENT).append("/**")                            .append(LINE_SEPARATOR)
                .append(INDENT).append(" * @serial ")                    .append(LINE_SEPARATOR)
                .append(INDENT).append(" */")                            .append(LINE_SEPARATOR)
                .append(INDENT).append("private final SubType subType;") .append(LINE_SEPARATOR)
                .append(LINE_SEPARATOR);
        }

        generateConstructors(sb);

        if (hasSubTypes()) {
            sb.append(LINE_SEPARATOR);
            sb
                .append(INDENT).append("/**")                                                   .append(LINE_SEPARATOR)
                .append(INDENT).append(" * Produces the exception {@link SubType subtype}.")    .append(LINE_SEPARATOR)
                .append(INDENT).append(" *")                                                    .append(LINE_SEPARATOR)
                .append(INDENT).append(" * @return the exception subtype. Never {@code null}.") .append(LINE_SEPARATOR)
                .append(INDENT).append(" */")                                                   .append(LINE_SEPARATOR)
                .append(INDENT).append("public SubType getSubType() {")                         .append(LINE_SEPARATOR)
                .append(DOUBLE_INDENT).append("return subType;")                                .append(LINE_SEPARATOR)
                .append(INDENT).append("}")                                                     .append(LINE_SEPARATOR)
                ;
        }

        if (extraCode != null) {
            sb.append(extraCode);
        }

        sb.append("}").append(LINE_SEPARATOR);

        return sb.toString();
    }
    // END SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore)

    /**
     * Generates code for "standard" Java exception constructors except
     * for the no-arg constructor. Omitting the no-arg forces developers
     * to provide either a message, a cause, or both.
     *
     * @param sb
     *
     *        A {@link java.lang.StringBuilder} into which generated code is
     *        written. This parameter can be thought of as an "IN/OUT"
     *        parameter because it contains the value produced by this method.
     *
     * @to.do
     *
     * <ul>
     *
     * <li>Hmmm... that's a lot of warning suppression... refactor?
     *
     * </ul>
     */
    // BEGIN SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore) justification: custom formatting, see above
    // BEGIN SuppressWarnings(Checkstyle.CyclomaticComplexity, Checkstyle.NPathComplexity, Checkstyle.MethodLength, Checkstyle.ExecutableStatementCount) justification: see above
    @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.ExcessiveMethodLength" })
    /*
     * PMD Warning Suppression Justification:
     *
     * Code that generates code is inherently complex in general. The code in
     * this method is has custom formatting that is indented to promote
     * clarity. Liberal commenting attempts to compensate for complexity.
     */
    private void generateConstructors(final StringBuilder sb) {
        /**
         * Two-tuple that binds a formal parameter type with a formal
         * parameter name.
         */
        // BEGIN SuppressWarnings(Checkstyle.JavadocMethod, Checkstyle.VisibilityModifier) justification: trivial class, use is confined to this method
        @edu.umd.cs.findbugs.annotations.SuppressWarnings(
            value = "SIC_INNER_SHOULD_BE_STATIC_ANON",
            justification = "class use is confined to this method"
        )
        class Parameter {
            final Class type;
            final String name;
            final String description;
            Parameter(final Class type, final String name, final String description) {
                this.type = type;
                this.name = name;
                this.description = description;
            }
        }
        // END SuppressWarnings(Checkstyle.JavadocMethod, Checkstyle.VisibilityModifier)

        // Re-usable parameter objects
        final Parameter msg   = new Parameter(String.class,    "msg",   "A message describing this error.");
        final Parameter cause = new Parameter(Throwable.class, "cause", "The error that caused this error.");

        /*
         * The set of "standard" Java exception constructor parameter
         * combinations. One constructor will be generated for each
         * parameter combination.
         *
         * Note that the no-arg constructor is omitted because throwing an
         * exception without either an error message or a cause is a bad
         * practice to be avoided.
         */
        final Parameter[][] parameters = new Parameter[][] {
//                { }, // no-arg constructor
            { msg, },
            { cause, },
            { msg, cause, },
        };

        /*
         * Produce constructor code for each combination of constructor
         * parameters.
         */
        boolean firstConstructor = true;
        for (Parameter[] parameterArray : parameters) {
            // Skip lines between constructors.
            if (firstConstructor) {
                firstConstructor = false;
            } else {
                sb.append(LINE_SEPARATOR);
            }

            // Start generating constructor documentation comment.
            sb
                .append(INDENT).append("/**")                                                 .append(LINE_SEPARATOR)
                .append(INDENT).append(" * Creates a {@code ").append(className).append("}.") .append(LINE_SEPARATOR)
                ;

            // Generate constructor parameter documentation.
            if (hasSubTypes()) {
                sb
                    .append(INDENT).append(" *")                                                         .append(LINE_SEPARATOR)
                    .append(INDENT).append(" * @param subType")                                          .append(LINE_SEPARATOR)
                    .append(INDENT).append(" *")                                                         .append(LINE_SEPARATOR)
                    .append(INDENT).append(" *        The exception sub type. May not be {@code null}.") .append(LINE_SEPARATOR)
                    ;
            }
            for (Parameter parameter : parameterArray) {
                sb
                    .append(INDENT).append(" *")                                       .append(LINE_SEPARATOR)
                    .append(INDENT).append(" * @param ").append(parameter.name)        .append(LINE_SEPARATOR)
                    .append(INDENT).append(" *")                                       .append(LINE_SEPARATOR)
                    .append(INDENT).append(" *        ").append(parameter.description) .append(LINE_SEPARATOR)
                    .append(INDENT).append(" *        May not be {@code null}.")       .append(LINE_SEPARATOR)
                    ;
            }

            // Finish generating constructor documentation comment.
            sb
                .append(INDENT).append(" */").append(LINE_SEPARATOR)
                ;

            // Start generating constructor signature.
            sb
                .append(INDENT).append("public").append(" ").append(className)
                .append("(")
                ;

            // Generate constructor formal parameter declarations.
            boolean firstParameter = true;
            if (hasSubTypes()) {
                firstParameter = false;
                sb.append("final SubType subType");
            }
            for (Parameter parameter : parameterArray) {
                if (firstParameter) {
                    firstParameter = false;
                } else {
                    sb.append(", ");
                }
                sb
                    .append("final ")
                    .append(parameter.type.getSimpleName())
                    .append(" ")
                    .append(parameter.name)
                    ;
            }

            /*
             * Finish generating constructor signature; Start generating
             * constructor body.
             */
            sb.append(") {").append(LINE_SEPARATOR);

            // Generate invocation of supertype constructor, if necessary.
            boolean firstArgument = true;
            for (Parameter parameter : parameterArray) {
                if (firstArgument) {
                    sb.append(DOUBLE_INDENT).append("super(");
                    firstArgument = false;
                } else {
                    sb.append(", ");
                }
                sb.append(parameter.name);
            }
            if (! firstArgument) {
                sb
                    .append(");")
                    .append(LINE_SEPARATOR)
                    ;
            }

            /*
             * Set the subtype field, if this exception specification has
             * subtypes.
             */
            if (hasSubTypes()) {
                sb.append(DOUBLE_INDENT).append("this.subType = subType;").append(LINE_SEPARATOR);
            }

            // Finish generating constructor body.
            sb.append(INDENT).append("}").append(LINE_SEPARATOR);
        }
    }
    // END SuppressWarnings(Checkstyle.CyclomaticComplexity, Checkstyle.NPathComplexity, Checkstyle.MethodLength, Checkstyle.ExecutableStatementCount)
    // END SuppressWarnings(Checkstyle.NoWhitespaceAfter, Checkstyle.NoWhitespaceBefore)

    /**
     * Produces a {@link java.io.File} that corresponds to an existing,
     * writable file. The pathname of the file is the value of the {@code
     * targetDirectory} parameter followed by a path computed from {@link
     * #packageName} and the {@link #className}, replacing dot (.) package
     * separator characters with platform appropriate {@link
     * java.io.File.separator file separator characters}.
     *
     * @param targetDirectory
     *
     *        The directory in which to produce an existing, writable
     *        {@code File}. May not be {@code null}.
     *
     * @return A {@code File} that corresponds to an existing, writable
     *         file. The path name of the file is the value of {@code
     *         targetDirectory} followed by a path computed from {@code
     *         packageName} and {@code className}. Never {@code null}.
     *
     * @throws IOException
     *
     *         Iff the file for code generation cannot be created or is not
     *         writable.
     */
    private File getTargetFile(final File targetDirectory) throws IOException {
        final String relativeTargetDirectoryPathname = packageName.replace('.', File.separatorChar) + File.separatorChar;
        final File absoluteTargetDirectoryFile = new File(targetDirectory, relativeTargetDirectoryPathname);
        final boolean directoryNotThere = (! absoluteTargetDirectoryFile.exists());
        if (directoryNotThere) {
            /*
             * There is a race condition here because the directory could be
             * created after checking for existence, but before invoking
             * mkdirs.
             */
            final boolean directoryNotThereAndCannotCreateIt = (! absoluteTargetDirectoryFile.mkdirs());
            if (directoryNotThereAndCannotCreateIt) {
                final String errMsg = MSGS.get(L7dMsg.DIRECTORY_CANNOT_BE_CREATED, absoluteTargetDirectoryFile);
                throw new IOException(errMsg);
            }
        }
        final String relativeTargetFilePathname = (relativeTargetDirectoryPathname + className + JAVA_EXTENSION);
        final File absoluteTargetFile = new File(targetDirectory, relativeTargetFilePathname);
        final boolean fileNotThere = (! absoluteTargetFile.exists());
        if (fileNotThere) {
            /*
             * There is a race condition here because the file could be
             * created after checking for existence, but before invoking
             * createNewFile.
             */
            boolean fileNotThereAndCannotCreateIt = false;
            try {
                /*
                 * createNewFile has two failure behaviors:
                 *
                 * 1. produce false
                 * 2. throw an IOException
                 */
                fileNotThereAndCannotCreateIt = (! absoluteTargetFile.createNewFile());
            } catch (final IOException cause) {
                final String errMsg = MSGS.get(L7dMsg.FILE_CANNOT_BE_CREATED, absoluteTargetFile);
                throw new IOException(errMsg, cause);
            }
            if (fileNotThereAndCannotCreateIt) {
                final String errMsg = MSGS.get(L7dMsg.FILE_CANNOT_BE_CREATED, absoluteTargetFile);
                throw new IOException(errMsg);
            }
        }
        final boolean cannotWriteToFile = (! (absoluteTargetFile.canWrite() || absoluteTargetFile.setWritable(true)));
        if (cannotWriteToFile) {
            final String errMsg = MSGS.get(L7dMsg.FILE_NOT_WRITABLE, absoluteTargetFile);
            throw new IOException(errMsg);
        }
        return absoluteTargetFile;
    }
}
